Cutthroats Unofficial Classic Patch - technical list of fixes
=============================================================

Every corrected site of the 64-bit rebuild of the English V1 P7 game, as recorded in the
source's docs/classic/decisions/*.csv, grouped by kind. CHANGES.txt is the readable summary.
Everything not listed behaves as it did in 1999.

Crashes
-------
* empty-terrain cannon order passed an uninitialized stack world.z into the ranged-attack producer and stored target state (undefined read)
* rowboat exit: original dereferences a NULL fleet leader, restores an already released pooled boat, and ends the campaign while a healthy primary ship remains
* teardown uses ResolveBattleRowboatExit instead of the original NULL-leader dereference and use-after-release restore
* view animations are reset before the descriptor/town AnBs that own their nodes are unloaded (use-after-free otherwise)
* original late ResetAllOriginalEntityViewAnimations after the owning animation chains were freed; paired with line 628
* class-4 release severs stale nearest/follow handles to a pooled, reusable unit record (use-after-release)
* call site of ClearRetiringUnitLinks in the class-4 release
* original writes g_townRecords[UINT32_MAX].field139 when no town is in range; classic writes the original 0xfb hostility byte only inside the 85-row table, DE drops the byte for impact attribution
* ship-source effect calls MarkOriginalBattleTownHostile, whose classic arm restores the bounded original byte write
* MarkOriginalBattleTownHostile declaration; classic arm of the helper restores the bounded original byte
* original g_townStandingAdjustment[town*0x214] write with town -1 (OOB); classic writes the original byte only for a real town
* same town -1 OOB write on the officer attack path; bounded original byte in classic
* same town -1 OOB write on the 0x12 attack path; bounded original byte in classic
* classic arm of the town hostility write: keeps the original 0xfb hostility byte but writes it only for a real town, never at index -1 outside the 85-row table
* Numeric labels were sprintf'd into the event pointer's 8-byte stack slot (stack corruption); a local buffer is used
* Cargo slider gated by the gold amount let the player select more cargo than exists; the commit then walked past the fleet end (NULL ship) and created treasure
* Bury commit: no malloc NULL check and an unbounded fleet walk (ship=fleetNext with no count/NULL limit); replaced by a live-stock check and bounded debit
* Divide-the-plunder share guarded against a weighted crew total that wraps to 0 (divide by zero)
* Unguarded 1.1 refactor: the rowing boat is created before stock/payment mutation, so a failed allocation no longer charges the player and then dereferences NULL; the erased preview is released once
* Include for the shared bounded victory cargo scan
* Single-ship first-row scan bounded; the original read past the cargo table when every row was empty
* All-ships first-row scan bounded against the table end
* Scroll-up scan bounded; the original walked index-1 below row 0
* Scroll-down scan bounded; the original walked past the last row
* Previous-ship rescan bounded
* Next-ship rescan bounded
* Single-ship mode-switch scan bounded; the original read past the cargo table
* All-ships mode-switch scan bounded
* Officer-slot availability helper stays under AUDIT_FIXES; the pending-appointment text helper moved to a PRODUCT_RULES block
* Empty or out-of-table officer row (index >= 60) could be selected; teardown then changed occupation state with no officer
* Same slot validation for row +1
* Same slot validation for row +2
* Same slot validation for row +3 (can pass the 60-slot officer table)
* Confirmation re-validates the selected officer slot before committing the occupation
* Identical crew-ratio divide guarded against a zero weighted crew total
* Shutdown without a menu teardown callback left the 13 overlay roots alive while their fonts/sprites were released (use after free at exit)
* Suspended-view font and sprite owners released before input/device teardown instead of late DirectDraw release
* Roster walks bounded by crew total walked off the end of the unit list (NULL dereference)
* negative or >5 town standing indexed before/after g_govFameStandingScoreTable[32] in the restored original admission roll
* externs for the 1.4 movie-suffix and speech-table resolvers
* manner speech lookup read past the national/alternate tables for result 6 (x64 next-owner pointer)
* label paired with the 1.4 speech resolver (reference body only)
* manner movie suffix through the bounded resolver
* cannon tier clamp (fleets >=216 cannons read past g_topicCannonTierScoreTable) kept in a classic #elif arm with the original score; GovernorProductScore under PRODUCT_RULES
* classic #elif arm of the split: original topic score with only the cannon-tier row clamp 0..17
* topic movie suffix: original one-slot shift selected speech or the next owner as a movie path
* topic speech lookup through the bounded resolver
* 10001..15000 peso bribe left a stale variant that indexes past g_gunsBribeQualityTable rows
* bribe movie suffix: original one-slot shift selected speech or the next owner as a movie path
* bribe speech lookup through the bounded resolver
* shared movie-suffix and speech resolvers: response 1..6 read past the six-entry suffix array and one-before speech tables
* resolver externs kept; product helper externs nested under PRODUCT_RULES
* ship-offer bound accepted the id past the listed ships (walked past the fleet ring and released the wrong ship)
* ship-offer movie suffix: original one-slot shift selected speech or the next owner as a movie path
* ship-offer speech lookup through the bounded resolver
* share-offer movie suffix through the bounded resolver
* share-offer speech lookup through the bounded resolver
* cargo-offer movie suffix through the bounded resolver
* cargo-offer speech lookup through the bounded resolver
* guns-offer movie suffix through the bounded resolver
* guns-offer speech lookup through the bounded resolver
* Declaration for the mission linked-entity release that runs before teardown (line 266).
* Bounded saved-buffer reader declaration; an over-long saved length otherwise overruns the destination.
* x64 entity image: an update/draw index of 112 or more read a stray pointer past g_entityActionCallbacks and later called it.
* Old mission text owners and linked entities are released while the old flagship still exists; the original left them dangling across ReleaseAllOriginalEntities.
* x86 entity image: same out-of-table callback index guard as line 182.
* Map-alert block read is bounded to the 0x68 bytes the writer saves.
* Movie-request block read is bounded to 0x1cc bytes.
* Plunder-gate block read is bounded to 0x38 bytes.
* Deploy-state block read is bounded to 0x418 bytes.
* Saved action callback index of 112 or more read a stray pointer that became the action callback.
* Zero-fill ran before the NULL test, so a corrupt length the allocator refused wrote through NULL.
* Bounded saved-buffer reader used by the x64 and TS-03 paths; never stores past the destination.
* Mission linked entities and text owners are released before ReleaseAllOriginalEntities while the old flagship still exists.
* Reference-build partner of line 44 (#if !CUTTHROATS_AUDIT_FIXES): the original late call, compiled out in both product editions.
* A failed arena allocation was memset through NULL; close the file and return NULL instead.
* Keep joinable handles for the pointer and palette workers so shutdown does not free UI/DirectDraw state under a running worker.
* Join both workers (waking a self-suspended palette worker) before shutdown releases their resources.
* activation passes a NULL control; the original stale-row loop dereferences it as a node whenever treasure rows exist
* selection flag written by raw control id (10000+) through an alias before the flag table; bounded zero-based index instead of an out-of-bounds write
* negative town standing indexes g_trialConvictionChanceTable before its start; clamp to the 6 authored columns (out-of-bounds read)
* bribe divisor of five rand()%200 terms can be 0: integer divide by zero; same draws
* governor attitude -5..-9 indexes g_clemencyChanceTable before its start; clamp (out-of-bounds read, natural layout has different neighbours anyway)
* period index 6 reads g_townTrialChanceTable past its last row (out-of-bounds read)
* occupation officer index >=60 reads past g_officers and an absent officer let occupation records mutate before the NULL check
* saved governor-name history index 64 writes 64 bytes past the 4096-byte table
* wrap at 64 instead of >64 so the 65th replacement does not write past the table into the palette globals
* entity view animations still name the town bundle; reset before ReleaseOriginalTownSceneResources (use after free)
* original late reset after the bundles were released (paired with 349)
* parley submenu under PRODUCT_RULES; classic #elif keeps only the NULL event-control guard before control->id
* Leave behind with every ship short released the whole fleet and left g_primaryShip dangling
* release loop restarted at the released primary/skip ships and could release the last ship (dangling g_primaryShip)
* delivered class-16 attachment slot cleared before the entity enters the free pool (dangling reference revisited or saved)
* delivered unit attachment slot cleared before detach/release (dangling reference revisited or saved)
* building marker slots borrow animation nodes; clear them before the town bundles are released (use after free)
* original late clear after release (paired with 257)
* entity view instances may still name the previous town AnB; reset before release (use after free)
* empty sea: FindOriginalNearestFlaggedShip returns NULL and the original reads its position
* WM_DESTROY/WM_CLOSE/WM_QUIT destroy the UI lock; release it before dispatch so shutdown does not use a deleted lock
* own the UI lock for the whole simulate/draw/flip so a worker transition cannot tear down state mid-frame
* original force-cleared the borrowed-surface bit so ReleaseOriginalControlMovie released a surface it did not own (double release)
* SmackClose(NULL) on a movie node without a Smacker handle
* detach a borrowed movie surface so a later release does not free it again
* pointer worker stops polling during transitions and after shutdown when g_uiRenderContext is NULL
* external text of exactly the allocation size wrote its terminator past the buffer
* 64-character inline text wrote its terminator over the adjacent node field
* the 32-slot image global cannot hold the original 64 suspended roots plus terminator (overflow)
* panel index bound and NULL context check plus the loader transition stay; the History handler assignment (1.6 feature) moves under CUTTHROATS_PRODUCT_RULES at line 148
* rejects a repeated close or close without a render context and opens the transition
* destroys overlay trees before the fonts and sprites they borrow are released (use after free)
* clears restored suspended-root slots so a later close cannot reactivate stale roots
* partial setup released the device here and again in the owner teardown (double release and double CoUninitialize)
* clears the global worker owner before the owner is freed (dangling pointer)
* disposes a failed audio owner once (pairs with the single teardown at audio_device_lifecycle.c:165) instead of leaking a half-built DirectSound device
* retires stale stream IDs and device pointer so a recreated owner cannot use freed streams
* releases a valid prior stream ID after a completed fade so the replacement does not orphan the stream
* skips releasing with a NULL owner or invalid stream ID
* a missing or one-entry loading font bundle was dereferenced (NULL/out-of-range entries+1)
* clears the released bundle so the shared exit path below cannot release it twice
* single release of the font bundle on the quit/BMP-failure exits (pairs with line 249)
* ReplayMode declaration for the worker join path
* DestroyAllOriginalControlRoots declaration for shutdown
* joins the pointer and palette workers before the surfaces/palettes/frames they borrow are freed
* does not resume the movie worker that live shutdown already joined (closed handle)
* retires suspended overlay roots before their fonts are freed
* releases ship entities before the ordinary bundles whose nodes and frames they borrow
* a zero supply need divided by zero and the ftol of infinity produced a wild clip rectangle
* clears entity and flag animations before the map AnB owner they borrow from is released
* original late reset touched animation nodes after their bundle was freed (use after free)

Freezes and endless loops
-------------------------
* unbounded spawn-point search hung the land battle when no free spot existed
* candidate bound of the CB-15 search
* exit label of the CB-15 bounded search
* a turret gunner outside every building polygon fired forever; it now terminates
* cannon removal deck walk looped forever when cannonCount exceeded the guns on the decks
* remainingBefore snapshot for the no-progress exit
* Daily kill loop made no progress when no unit matched and never ended
* Removal tested quantity only after whole passes; a request larger than the men aboard or an unknown category looped forever
* An unknown saved view id initialised no view and left a black screen the player could not leave; it now opens the Caribbean map.
* dismiss helper used by the 0x200000 close below (and native/runtime/map_pins.c); restores the map handler and simulation
* original 0x200000 close of OnArrival2 left the map root with a NULL handler (map takes no input); the fix closes the optional choice with input restored and keeps the required one open
* fade wait exits on shutdown (the stopped palette worker never clears the flag) and services SDL input while waiting
* a failed worker start left DestroyOriginalAudioOwner waiting forever for a stop acknowledgement; x64 handle test uses UINTPTR_MAX

Saving and loading
------------------
* fixed-width CTRELAT1 encode/decode kept shared and pure so any save writer still compiled in emits a well-formed zero block
* Declaration of the whole-file preflight that refuses a bad save before the campaign is torn down.
* A slot that cannot be opened is reported instead of failing silently.
* Validate and snapshot the complete original grammar before teardown; a short or corrupt file no longer destroys the running campaign or its dock.
* Dock record is cleared only after the identifier check, so a refused load leaves the running dock intact.
* SaveGuardCheckMinimumLoad declaration used by the Hall of Fame read (the GameplayQol declaration beside it is inert)
* short HallOfFame.dat XORed stack residue into all ten live rows
* rejects a short file and stages all ten reads before mutating the live table

Lost or duplicated gold, items, crew and ships
----------------------------------------------
* Unguarded 1.1.2 refactor: affordability preview shares the whole-peso bill quote used by the settlement, so the bill cannot pay for more than the preview allowed (original preview drift left in Git history)
* Live-stock whole-unit bury commit helpers stay under AUDIT_FIXES; the slider-extent macro moved under a nested PRODUCT_RULES arm with the original range in the classic #else
* Available valuables counted in whole units per source so the dialog matches the live-stock commit; the float sum let a map record more than could be debited
* Same whole-unit availability for each ship's valuables
* Stale gold/cargo selections are cleared on open so a reopened dialog cannot bury an old selection the fleet no longer holds
* Original reset the gold amount twice and never the cargo amount, so a stale cargo selection was buried from stock that may not exist
* Declarations only (QoL message text, class-16 detach/list insert); no behaviour of their own
* A share of exactly 500 or 2400 kept a stale outcome byte and ended a first career on the fleet-destroyed screen with no Resume (campaign lost)
* Unguarded 1.1.2 settlement: whole bill preflighted against live stock and cash, dock stock sold first, payment for the quantity actually removed (subsumes EC-01, whose pooled-dock arm paid nothing); stale bills could pay for cargo that no longer existed
* A sold ship's carried pesos were zeroed before deposit and never reached the hoard
* Declarations for the FC-02 class-16 item transfer
* Unguarded 1.1 refactor: sold-ship cargo/crew transfer, stock, payment and vector removal committed in one transaction before UI refresh
* A sold ship's rowing boats stayed linked after being queued on the dock, so buying the ship back duplicated them
* A sold ship's class-16 items left with it into shipyard stock; they move to the dock list (and FC-01 captures goldCarried before it is zeroed)
* Grenade row per-ship availability read the pistol slot; the consumer clamps to the real grenades but credits the full selection, duplicating grenades
* Dock total omitted the item tally and uncounted participant units, so items or those units alone never triggered the leave-port warning and were wiped on departure
* A sailor remainder that fitted was never placed, and Move to ships then wiped it
* Jewels (which never use hold load) were rejected at a full hold and the caller then cleared the untransferred pending prize record
* Victory prize cash and selected cargo pending for ships excluded for crew shortage were never settled and were discarded
* All of a ship's rowing boats are unlinked but the original queues one dock record from an uninitialised stack buffer; the other boats vanish and the record is garbage
* The dock box opens this box without the sidebar's all-short test; with every ship short Leave behind released the whole fleet, primary ship included (fleet loss / dangling g_primaryShip)
* After Leave behind the original departs without the dock check the other exits run; the dock record is wiped at the next town arrival, so goods and men left there are lost unwarned
* Original save writes inactive shipyard previews, restore leaves them unowned; they enter battle as crewless player boats at the origin and the orphan-boat teardown ends the career with a healthy fleet (also an entity leak per load). Not morale-related
* original_unit_types.h kept for FC-14; crew_morale.h and governor_relations.h moved under PRODUCT_RULES
* externs for the FC-14 unit/boat/item transfer
* gifted ship released its officers/passengers/prisoners/rowing boats/items with it
* Declaration for the orphaned shipyard-preview cleanup (line 1199).
* Other half of the split: restored orphan shipyard preview ships became battle participants and caused a false whole-fleet-loss career ending.
* acceptance loop re-indexes after unlinking so unselected treasure maps are spent (select 1 of 3: all 3 removed)
* map Move to ships cleared the dock after a partial load, destroying cargo the player chose to keep (the port branch already reopens)

64-bit correctness (crashes and corruption on modern Windows)
-------------------------------------------------------------
* original indexes the nine-entry g_shipClassLabels with variants 21..30 (OOB on x64 globals); the fix reads the same 0x00505914 equipment strings the x86 image did
* class-6 spacing indexed hull table [6] past its six entries; DE uses the 260 fallback radius, classic reads the original image's [6] value (g_shipClassEngageRanges[0]) explicitly
* groupTag indexed the nine-entry ship-label table (OOB on x64 globals); DE shows the unit type name, classic keeps the original label for groupTag<9 and falls back to the type name only beyond the table
* natural x64 globals split the eight-byte clock, so date[4..5] read unrelated storage instead of day and hour
* fleet leader promotion copied x86 byte offsets 0x5f4..0x601 over unrelated fields of the widened x64 ship record
* same x86-offset copy of 0x602..0x604 replaced by the named members
* class-6 motion indexed hull table [6] past its six entries; DE uses the 260 fallback radius, classic reads the original image's [6] value (g_shipClassEngageRanges[0]) explicitly
* Y distance uses the contactRadius chosen by the split at line 73 in both editions
* record-type includes for the typed terminated-field read below
* read the target unit's terminated byte by member, not the x86 offset +0x2c7 of the widened x64 record
* same x86 +0x2c7 offset read replaced by the named member
* patrol links are native pointers; the x86 +0x110/+0x114 slot offsets read unrelated fields on x64 and crashed battle startup
* Rowing-boat class read through ship->shipClass instead of the x86 literal offset 0x5c4, which on the natural layout read another field (false fleet-loss endings)
* Same shipClass field instead of literal 0x5c4 for the active-ship walk
* Marque fleet-type and fields5d9 written through named fields instead of x86 literal offsets 0x5da..0x5de
* DE keeps the fractional-cargo emphasis; classic keeps the original whole-ton Ftol test but walks the 14 pooled floats by index instead of the (int32_t)-truncated address range to &g_fleetSupplySharePerShip
* classic only: the original +3 shift reads rows 26..27 of the 26-row g_governorResponseCodes; read the x86 next owner g_governorVisitScoreTierTable by name (DE unchanged)
* In the natural x64 layout the live clock day is not at g_currentDate+4, so the date text read a neighbouring global.
* Unsigned DWORD negation added 4 GB to a pointer on x64, so the escort text copy read far outside the string.
* Same 4 GB pointer negation defect on the second escort text path.
* DE calendar-day compare under PRODUCT_RULES; classic #elif keeps the original exact eight-byte snapshot==mirror compare but reads each word from its own object, because the raw walk past g_trialDateCurrent and the 4-byte mirror pack reads unrelated memory in the natural layout
* literal x86 0x8c stack layout: row text at +12 overwrites the high half of the 8-byte list pointer (crash in AppendOriginalScrollListItem)
* event control kept at native width instead of a truncated 32-bit slot later dereferenced as a pointer
* negated unsigned DWORD length added to a pointer adds 4 GiB on x64 (access violation in the arrival speech copy)
* negated unsigned DWORD length added to a pointer adds 4 GiB on x64 (arrival speech append)
* negated unsigned DWORD length added to a pointer adds 4 GiB on x64 (reason 3 speech copy)
* negated unsigned DWORD length added to a pointer adds 4 GiB on x64 (reason 3 speech append)
* negated unsigned DWORD length added to a pointer adds 4 GiB on x64 (reason 2/4 speech copy)
* guilty verdict reason 1: negated DWORD length adds 4 GiB to the source pointer (reported crash family)
* guilty verdict reason 3: negated DWORD length adds 4 GiB to the source pointer
* guilty verdict reason 2/4: negated DWORD length adds 4 GiB to the source pointer
* acquittal reason 1: negated DWORD length adds 4 GiB to the source pointer
* acquittal reason 2/4: negated DWORD length adds 4 GiB (the player's 0xc0000005 in HandleOriginalTrialAcquittal)
* acquittal reason 3: negated DWORD length adds 4 GiB to the source pointer
* rows were a stack array filled only by the init event; click/confirm events read uninitialised stack (on x64 not the x86 leftovers) as selection flags and prices, buying back or charging for random ships
* zero the persistent row prices with their flags so unused rows cannot add garbage to the buy-back total
* new classic-only arm: search result 0xff read town record 255, about 90 KB past the natural 85-record table; read the same x86 word (0x0060f18c, governor record 1148 byte 8) so the original return-or-continue decision is kept without the overrun
* next_town label shared by the QoL skip and the classic town-255 arm

Modern Windows: responsiveness, input and sound
-----------------------------------------------
* diagnostic ReferenceLog declaration only (16 capped lines); no gameplay effect, kept for crash/input reports
* diagnostic battle-select release log; no gameplay effect
* ReferenceLog declaration for the battle-entry/exit diagnostics; no gameplay effect
* battle-entry diagnostic ReferenceLog; no gameplay effect
* diagnostic log inside the rowboat-exit helper; no gameplay effect
* diagnostic rectangle-selection log state; no gameplay effect
* diagnostic counters only
* diagnostic sample capture only
* diagnostic ReferenceLog output (16 capped lines)
* ReferenceLog declaration for a diagnostic; no gameplay effect
* same ActivateOriginalControlRoot call plus a map-arrival diagnostic log; behavior identical
* last-primary-detach diagnostic log; no gameplay effect
* close the still-playing adviser Smacker handle before clearing its request pool so the stream is not orphaned behind the governor
* Under Wine and Proton the launcher's subst mapping silently maps nothing, so the game waited forever for its CD; CUTTHROATS_CD_ROOT answers the CD check from the game folder
* Declaration of the CD root redirect
* File opens on the stand-in CD drive are redirected to the game folder when CUTTHROATS_CD_ROOT is set (Wine and Proton)
* Include split: view_transition.h stays for the host-serviced staged restore, crew_morale_save.h moved to a CUTTHROATS_PRODUCT_RULES block at line 13.
* Transition phase keeps the SDL host serviced while the restore runs.
* Ends the host-serviced transition opened before the preflight.
* Read-only opens count as transition resources so long loads keep servicing the SDL host.
* Declaration of the main-thread handle the worker join closes.
* Keep servicing the host window while an overlay handler on the pointer worker owns the UI lock instead of parking the window thread.
* declarations for the window-servicing wait
* live/replay switch so recordings keep the original clock call sequence
* tick-alignment busy wait services the SDL window instead of spinning
* one-second CPU calibration busy wait services the window (unresponsive host otherwise)
* view_transition.h for the same-thread loader continuation that keeps the SDL window serviced
* WindowPacedSleep declaration for the frame limiter wait
* startup asset loading yields to the SDL host instead of freezing the window
* ends the startup loader transition scope opened above
* acquire the input lock before PeekMessage so a worker transition flush cannot leave a stale local message
* releases the per-message input lock when the queue is empty
* releases the per-message input lock after dispatch
* releases the frame lock on the paused/bypass path
* loader transition scope for view changes (the captured speed is read only by the inert QoL branch)
* loader phase marker so the host keeps pumping during teardown/initialization
* loader phase marker
* ends the view-change transition scope
* captures the view/speed so a tick batch can stop after yielding input between accelerated ticks
* yield pointer input between accelerated ticks (64x held the input lock for 64 ticks) and stop if the view changed meanwhile
* releases the frame lock after Flip
* frame limiter wait services SDL input and presents in short slices instead of a blind Sleep
* view_transition.h for transition-aware pointer polling
* input lock and button reset entry points used by the loader transition
* view_transition.h for panel open/close transitions
* ends the panel-open transition scope
* ends the panel-close transition scope
* view_transition.h for fade waits
* WindowPacedSleep declaration for fade waits
* balances timeBeginPeriod even when no timer was installed so the system timer resolution is restored
* WindowPumpDuringFade declaration
* caps the blocking music fade at twelve display ticks so a view change does not stall the window thread for about 1.7 s
* releases the surface lock while waiting so the pointer worker can draw and pumps SDL input between ticks
* removes the transient post-start mute that let the SDL mixer callback output a silent block on a new cue
* runs the game on a fiber so screen loaders yield to the SDL host
